/**
* The implementation of DDAS_Work_Handler, the class responsible for performing the function of work generation for the DDAS project
* 
* @author Max Rupplin
* @date 5-18-08
*/

//Long Integer Package
extern "C"
{
        #include "lip.h"
}

//DDAS related
#include "DDAS_Work_Handler.hpp"
#include "DDAS_DB_Handler.hpp"
#include "DDAS_XML_Handler.hpp"
#include "ddas_workgen.h"

//BOINC related
#include <sched_util.h>
#include <backend_lib.h> 
#include <boinc_db.h> 
#include <parse.h>
#include "sched_msgs.h"

//C/C++ related
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <fstream>
#include <iostream>

//namespaces
using namespace std;

        
/** 
* Default constructor
*/
DDAS_Work_Handler::DDAS_Work_Handler(){}

/**
* Default destructor
*/
DDAS_Work_Handler::~DDAS_Work_Handler(){}


/**
* @author Max Rupplin
* @date 5-18-08
* @param state_file_names A pointer to char** that stores the names of the state files 
* @param scoping_file_names The scoping file names from which the state file names are derived
* @param starting_value The iteration, in terms of work generation, that we are starting from
* @param ending_value The last iteration, in terms of work generation, that we are interested in
* @return Returns 1
*
* 'get_related_state_files' uses the scoping_file_names to create the names of the state files
*/
int DDAS_Work_Handler::get_related_state_files( char*** state_file_names, char** scoping_file_names, long starting_value, long ending_value )
{

	try
	{
		//what to append
        	char* state = "_state_file";

        	//for each scoping file (result) we need to create a state_file
        	for( long i=starting_value; i<ending_value; i++)
        	{
unsigned long index = i-starting_value;

			//alloc some space for the final string
			(*state_file_names)[index] = new char[128];

        	        //easier for appending
        	        string tmp (scoping_file_names[index]);

        	        //append "state_file" to end of scoping file name (job_68_wu_1_of_1 -> job_68_wu_1_of_1_state_file)
        	        tmp.append(state);

			//copy appended string to state_file_names	
			strcpy( (*state_file_names)[index], tmp.c_str() );
		}
	}
	catch(char* ex)
	{
		return 0;
	}

	return 1;
}

/**
* @author Max Rupplin
* @date 5-18-08
* @param state_file_names The names of the state files, from which the output URL is derived
* @param starting_value The first iteration, in terms of work generation, for this function
* @param ending_value The last iteration, in terms of work generation, for this function
* @return 1 if successful
*
* 'write_state_files_to_download_dir' writes empty files with the names specified in 'state_file_names'
*/
int DDAS_Work_Handler::write_state_files_to_download_dir( char** state_file_names, long starting_value, long ending_value )
{
        #ifdef DEBUG_MODE
        cout<<"write_state_files_to_download_dir was called."<<endl;
        #endif

	//for each scoping file (result) we need to create a state_file
	for( long i=starting_value; i<ending_value; i++)
	{
unsigned long index = i-starting_value;

		//alloc some space
		char* result = new char[1024];

        	//figure out where to write the file
        	dir_hier_path
        	(
                	state_file_names[index],
                	"/home/boinc/projects/ddas/download",
                	1024,
                	result,
                	true
        	);

        	//write the data to file
        	write_to_file("", result);

		//free memory 
		delete[] result;
	}

        #ifdef DEBUG_MODE
        cout<<"write_state_files_to_download_dir is returning 1."<<endl;
        #endif

        //the hashed location of the file (see BOINC API)
        return 1;
}


/**
* Given a location, defined by 'path' and some data, defined by 'data', write 'data' to a file at the location specified by 'path'.
* 
* @param data The data to be written to file.
* @param path The location and name of the file as a char pointer. (e.g. /home/user/file.txt)
* @return Returns 1 if successful, 0 otherwise.
*/
int DDAS_Work_Handler::write_to_file(char* data, char* path)
{
	#ifdef DEBUG_MODE
	cout<<"write_to_file() called."<<endl;
	cout<<"write_to_file data: "<<endl<<data<<endl;
	cout<<"write_to_file path: "<<endl<<path<<endl;
	#endif

        try
        {
		//open file
                ofstream myfile (path);

		//if opened write data to file
                if(myfile.is_open())
                {
		    myfile<<data;
                    myfile.close();
                }

                #ifdef DEBUG_MODE
                cout<<"write_to_file() returning 1."<<endl;
                #endif

                return 1;
        }
        catch(char* e)
        {
                #ifdef DEBUG_MODE
                cout<<"write_to_file() returning 0."<<endl;
                #endif

                return 0;
        }
}

/**
* Given a file located at 'path', return its size in bytes.
* 
* @param path The location and name of the file. (e.g. /home/user/file.txt)
* @return Returns the size of the file as a long.
*/
long DDAS_Work_Handler::get_file_size(char* path)
{
	long begin;
	long end;	
	ifstream myfile (path);
  
	begin = myfile.tellg();
 	myfile.seekg (0, ios::end);
  	end = myfile.tellg();
  
	myfile.close();

	#ifdef DEBUG_MODE
	cout << path <<"'s size is: " << (end-begin) << " bytes.\n";
	#endif

  	return (end-begin);
}


/**
* Given a problem ID (pid), copy the associated data file's contents into 'content' and its name to 'name'.
*
* @param pid The problem ID: this is the unique identifier for a job.
* @param content The content of the data file. This should contain comma seperated values defining the independent and dependent variables for the regressions.
* @param name The name of the data file.  If the data file were /home/user/3.csv, 'name' would be set to 3.csv.
* @return Returns 1 if successful, 0 otherwise.
*/
int DDAS_Work_Handler::get_related_data_file(int pid, char** content, char** name)
{
	try
	{
		string data;
		string line;

		char buffer[256];
		char tmp[256];

		//create a URL pointing to the location of the job's data file
		sprintf(buffer, "/home/boinc/projects/ddas/problem/%u.csv", pid);

		//alloc some memory for the content
		int file_size = get_file_size(buffer);
		*content = new char[file_size];
		
		//alloc some memory for the name
		sprintf(tmp, "%u.csv", pid);
		*name = new char[strlen(tmp)];

		//open the URL
  		ifstream myfile (buffer);

		//if successfully opened, copy file to 'data'
		if (myfile.is_open())
  		{
    			while (! myfile.eof() )
    			{
      				getline (myfile,line);
				data.append(line);
    			}
    		
			myfile.close();
  		}

		//move string to char*
		strcpy(*content, data.c_str());
		
		//copy 'tmp' to 'name'
		strcpy(*name, tmp);

		//success
		return 1;
	}
	catch(char* e)
	{
		//failure
		return 0;
	}
}


/**
* Given a problem ID (pid), create the necessary scoping files.  Store the names, content of the scoping files and how many were created in 'names', 'content' and 'size' respectively.
*
* @param pid The problem ID - the unique identifier for a job
* @param content The destination of the contents of the scoping files created herein
* @param names The destination of the names of the scoping files to be created herein
* @param starting_value The first iteration, in terms of work generation, for this job
* @param ending_value The final iteration, in terms of work generation, for this job
* @return Returns 1 if successful, 0 otherwise.
*/
int DDAS_Work_Handler::get_related_scoping_files(int pid, char*** content, char*** names, unsigned long starting_value, unsigned long ending_value )
{
	//variables to be set by init_from_database
	long numX=0;
	long numY=0;		
	long numRows=0;
	long numResults=0;
	long problemId=0;
	long jobID=0;
	long uid=0;
	long quorumSize=0;

        verylong workunit_count=0;
        verylong workunit_stride=0;
        verylong total_combinations=0;

	DDAS_DB_Handler db_handler;
	DDAS_XML_Handler xml_handler;

	//initialize required vars
	db_handler.initWorkgenVars( pid, &uid, &numX, &numY, &numRows, &numResults, &quorumSize, &workunit_count, &workunit_stride, &total_combinations );


	#ifdef DEBUG_MODE
	cout<<"pid: "<<pid<<endl;
	cout<<"uid: "<<uid<<endl;
	cout<<"numX: "<<numX<<endl;
	cout<<"numY: "<<numY<<endl;
	cout<<"numRows: "<<numRows<<endl;
	cout<<"numResults: "<<numResults<<endl;
	cout<<"quorumSize: "<<quorumSize<<endl;
        cout<<"jobID:"<<jobID<<endl;
        cout<<"total_combinations:";    zwriteln(total_combinations);
        cout<<"workunit_stride:";       zwriteln(workunit_stride);
        cout<<"workunit_count:";        zwriteln(workunit_count);
        cout<<"job_dimension:";         zwriteln(job_dimension);
	#endif

	try
	{

	        //for each workunit create a unique scoping file in XML format
	        for(unsigned long l=starting_value; l<ending_value; l++)
	        {
			//creates a relative index starting at 0 
			unsigned long index = l-starting_value;

        		//stores the name of the scoping file
			char* tmp = new char[256];
	
			//starting and ending values for a given iteration (written to scoping file)
                	verylong start=0;
                	verylong end=0;

                	//create unique string to represent scoping file name given inputs job_id and workunit_count
                	sprintf(tmp, "job_%d_wu_%d_of_%d", pid, l, ztoint(workunit_count) );

			//copy the name of the workunit to names[]
			(*names)[index] = new char[strlen(tmp)];
			strcpy((*names)[index], tmp);

                	//if this is the final iteration
                	if( zscompare(workunit_count, (index+1))==0 )
                	{
                		//start is a function of iteration
                        	zsmul( workunit_stride, index, &start);
				
				//start should be (l*workunit_stride)+1 
				zsadd( start, 1l, &start);

				//end should be total_combinations - 1 (2^8 = 256 combinations but 255 is the last index)
				zsadd( total_combinations, -1l, &end);
				
				//allocate space for scoping string
				(*content)[index] = new char[8192];

                        	//persist to char*
				strcpy( (*content)[index], xml_handler.getCustomFileAsString( start, end, numX, numY, numRows, numResults, pid ) );
            		}
                	else
                	{
                		//start and end are a function of the current iteration and the workunit_stride
                        	zsmul( workunit_stride, index, &start);
                        	zsmul( workunit_stride, (index+1), &end);

				//start should be (index*workunit_stride)+1 
				zsadd( start, 1l, &start);

				//allocate space for scoping string
				(*content)[index] = new char[8192];

                        	//copy to char*
				strcpy( (*content)[index], xml_handler.getCustomFileAsString( start, end, numX, numY, numRows, numResults, pid ) );
             		}

			//free memory
                	delete[] tmp;
			zfree(&start);
			zfree(&end);
        	}
	}
	catch( char* ex)
	{
		return 0;
	}

	return 1;
}


/**
* Writes the file named by 'name' with content defined by 'content' to file. It uses the BOINC API call, 'dir_hier_path' to determine its final location in the BOINC file hierarchy.
*
* @param content The file content - in this case the data file dropped off by the researcher
* @param name The name of the file to be written (e.g. 3.csv). The path should be excluded - just the actual name of the file is all that is required
* @return Returns 1 if successful, 0 otherwise.
*/
char* DDAS_Work_Handler::write_data_file_to_download_dir(char* content, char* name)
{
	#ifdef DEBUG_MODE
	cout<<"write_data_file_to_download_dir was called."<<endl;
	cout<<"content: "<<endl<<content<<endl;
	cout<<"name: "<<endl<<name<<endl;
	#endif

	//the absolute path of the file in the hierarchical scheme
	char* result=new char[1024];

	//figure out where to write the file
	dir_hier_path
	(	
		name,
		"/home/boinc/projects/ddas/download",
		1024,
		result,
		true		
	);

	//write the data to file
	write_to_file(content, result);

	#ifdef DEBUG_MODE
	cout<<"write_data_file_to_download_dir is returning: "<<result<<"."<<endl;
	#endif

	//the hashed location of the file (see BOINC API)
	return result;
}


/**
* Writes the files named by 'names' with contents defined by 'files' to file.  It uses the BOINC API call, 'dir_hier_path' to determin the final location of the files based on their names.
* 
* @param files The content of the scoping files
* @param names The names of the scoping files - names[X] corresponds to files[X]
* @param starting_value The first iteration, in terms of work generation, for this job
* @param ending_value The final iteration, in terms of work generation, for this job
* @return Returns the final location of the files based on their names.
*/
char** DDAS_Work_Handler::write_scoping_files_to_download_dir( char** files, char** names, unsigned long starting_value, unsigned long ending_value )
{
        #ifdef DEBUG_MODE
        cout<<"write_scoping_files_to_download_dir was called."<<endl;
        #endif

        //the absolute path of the file in the hierarchical scheme
        char** results = new char*[ending_value-starting_value];

	//for each scoping file
	for(long i=starting_value; i<ending_value; i++)
	{
unsigned long index = i-starting_value;

		//alloc memory
		results[index] = new char[1024];

        	//figure out where to write the file
        	dir_hier_path
        	(
                	names[index],
                	"/home/boinc/projects/ddas/download",
                	1024,
                	results[index],
                	true
        	);

		//write scoping file to hashed location
		write_to_file(files[index], results[index]);
	}

        #ifdef DEBUG_MODE
        cout<<"write_scoping_files_to_download_dir is returning."<<endl;
        #endif

	//array of hashed file locations (see BOINC API)
        return results;
}


/**
* Creates and inserts workunits for a job (defined by its problem ID, 'pid'). Uses a BOINC API call 'create_work' to do actual insertion.
*
* @param pid The problem ID - the unique identifier for a job
* @param data_file_name The name of the data file for this job
* @param scoping_file_names The names of scoping files for this job
* @param state_file_names The names of the state files for this job
* @param starting_value The first iteration, in terms of work generation, for this function
* @param ending_value The final iteration, in terms of work generation, for this function
*/
void DDAS_Work_Handler::create_workunits(int pid, char* data_file_name, char** scoping_file_names, char** state_file_names, unsigned long starting_value, unsigned long ending_value)
{
        #ifdef DEBUG_MODE
        cout<<"create_workunits was called."<<endl;
	cout<<"# of scoping files: "<<scoping_file_names_size<<endl;
	cout<<"data_file_name: "<<data_file_name<<endl;
	cout<<"scoping_file_names: "<<scoping_file_names[0]<<endl;
        #endif

	//for a DB call
	DDAS_DB_Handler* db_handler = new DDAS_DB_Handler();

	//BOINC helper objects
	DB_APP app;
	DB_WORKUNIT wu;
	SCHED_CONFIG config;

	//holds the workunit template
	char* wu_template;
		
	//initialize config based on config.xml
	config.parse_file("..");

	//BOINC utility function to open database
        int retval = boinc_db.open(config.db_name, config.db_host, config.db_user, config.db_passwd);
        if (retval)
        {
                log_messages.printf
                (
                        SCHED_MSG_LOG::MSG_CRITICAL,
                        "DDAS_Work_Handler : create_workunits : unable to connect to database.\n"
                );
		
		return;
        }
	else
	{
		#ifdef DEBUG_MODE
		cout<<"create_workunits successfully connected to the database."<<endl;
		#endif
	}

	//initialize DB_APP from database
	app.lookup("where name = 'ddas'");

	//read file and copy it to 'wu_template'
	read_file_malloc(WU_TEMPLATE_URL, wu_template);

	//initialize MYSQL later use
        MYSQL* mysql = mysql_init(0);
        
	//error detection
	if (!mysql)
        {
                log_messages.printf
                (
                        SCHED_MSG_LOG::MSG_CRITICAL,
                        "DDAS_Work_Handler : create_workunits : unable to initialize MYSQL.\n"
                );
		
		//close connection
		mysql_close(mysql);
	
		return;
        }

	//connect to the database
        mysql = mysql_real_connect( mysql, DBHOST, USERNAME, PASSWD, DBNAME, 0, 0, 0 );

	//error detection
	if( mysql==0 )
	{
		log_messages.printf
                (
                        SCHED_MSG_LOG::MSG_CRITICAL,
                        "DDAS_Work_Handler : set_job_to_processed : unable to connect to database.\n"
                );

                #ifdef DEBUG_MODE
                cout<<"MYSQL successfully connected to database..."<<endl;
                #endif

		//close connection
		mysql_close(mysql);
		
		return;
	}
	else
	{
		#ifdef DEBUG_MODE
		cout<<"MYSQL successfully connected to database..."<<endl;
		#endif
	}

	//for each scoping file 
	for(long i=starting_value; i<ending_value; i++)
	{
unsigned long index = i-starting_value;

		char* buffer = new char[1024];
		const char** infiles = new const char*[3];
		infiles[0] = data_file_name;
		infiles[1] = scoping_file_names[index];
		infiles[2] = state_file_names[index];

		//find quorum size 
		int quorumSize = db_handler->getQuorumSize(pid); 

		//create a BOINC workunit
		wu.clear();
 		strcpy(wu.name, scoping_file_names[index]);
    		wu.appid = app.id;
    		wu.min_quorum = 		(1 * quorumSize);
    		wu.target_nresults 	= 	(1 * quorumSize);
    		wu.max_error_results 	= 	(3 * quorumSize);
    		wu.max_total_results 	= 	(3 * quorumSize);
    		wu.max_success_results 	=	(3 * quorumSize);
    		wu.rsc_fpops_est 	= 	5e13;
    		wu.rsc_fpops_bound 	= 	1e15;
    		wu.rsc_memory_bound 	= 	1e8;
    		wu.rsc_disk_bound 	= 	1e8;
    		wu.delay_bound 		= 	7*86400;


		//call BOINC's create_work
		create_work
		(
        		wu,
        		wu_template,
        		"templates/ddas_result",
        		"../templates/ddas_result",
        		infiles,
        		3,
        		config
    		);

		//update the 'workunit_to_job_mapping' table for custom DDAS backend logic
                sprintf( buffer, "insert into workunit_to_job_mapping (wu_id, job_id) values ('%u', '%u');", wu.id, pid);

		//SQL query + error handling
		if( mysql_query(mysql, buffer) )
		{
			log_messages.printf
			(
				SCHED_MSG_LOG::MSG_CRITICAL,
                        	"DDAS_Work_Handler : create_workunits : mysql_query failed.\n"
                	);

                	#ifdef DEBUG_MODE
                	cout<<"MySQL query failed:";
                	cout<<"create_workunits is returning."<<endl;
                	#endif

			//close connection
			mysql_close(mysql);

			return;
        	}

		
		//free memory
		delete[] buffer;
		delete[] infiles;
	}

	//close connection
	mysql_close(mysql);

	//free memory
	delete[] wu_template;
	delete db_handler;

	#ifdef DEBUG_MODE
        cout<<"create_workunits is returning."<<endl;
        #endif
}

/** 
* Checks if new work (a new job) has been dropped off by a researcher by querying 'ddas_problem' in the DDAS database. This function exists to provide a quick exit mechanism for the executable in the typical case where new work has not been dropped off.
* 
* @return Returns 1 if new work exists, 0 otherwise
*/
int DDAS_Work_Handler::exists_new_work()
{
        #ifdef DEBUG_MODE
        cout<<"exists_new_work was called."<<endl;
        #endif

	//initialize MYSQL
 	MYSQL* mysql = mysql_init(0);

	//error handling
        if (!mysql)
        {
                log_messages.printf
                (
                        SCHED_MSG_LOG::MSG_CRITICAL,
                        "DDAS_Work_Handler : exists_new_work : unable to initialize MYSQL.\n"
                );

                #ifdef DEBUG_MODE
                cout<<"MYSQL initialization failed."<<endl;
		cout<<"exists_new_work is returning."<<endl;
                #endif

		//close connection
		mysql_close(mysql);

                return 0;
        }

	//connect to the DB
        mysql = mysql_real_connect(mysql, DBHOST, USERNAME, PASSWD, DBNAME, 0, 0, 0);

	//error handling
        if (mysql == 0)
        {
                log_messages.printf
                (
                        SCHED_MSG_LOG::MSG_CRITICAL,
                        "DDAS_Work_Handler : exists_new_work : unable to connect to the database.\n"
                );

                #ifdef DEBUG_MODE
                cout<<"MySQL connection failed."<<endl;
		cout<<"exists_new_work is returning."<<endl;
                #endif

		//close connection
		mysql_close(mysql);

                return 0;
        }
        else
        {
                #ifdef DEBUG_MODE
                cout<<"MySQL connection established."<<endl;
		cout<<"exists_new_work is returning."<<endl;
                #endif
        }

	//perform query + error handling
        if(mysql_query(mysql, "select problemId from ddas_problem where status=0"))
        {
                log_messages.printf
                (
                        SCHED_MSG_LOG::MSG_CRITICAL,
                        "DDAS_Work_Handler : exists_new_work : mysql_query failed.\n"
                );

                #ifdef DEBUG_MODE
                cout<<"MySQL query failed."<<endl;
		cout<<"exists_new_work is returning."<<endl;
                #endif
	
		//close connection
		mysql_close(mysql);

                return 0;
        }
                
	//store result of query
	MYSQL_RES* result = mysql_store_result(mysql);

	//if 1+ rows 
	if(mysql_num_rows(result)>0)
	{
		#ifdef DEBUG_MODE
                cout<<"exists_new_work has found new problem(s)."<<endl;
                #endif

		//close connection
		mysql_close(mysql);
		
		return 1;
	}

	//close connection
	mysql_close(mysql);

	#ifdef DEBUG_MODE
	cout<<"exists_new_work found no new work."<<endl;
	#endif

	return 0;
}
